package jmine.tec.security.ldap;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import jmine.tec.security.ldap.connector.LdapConnector;
import jmine.tec.security.ldap.exception.AuthenticationException;
import jmine.tec.security.ldap.exception.ConnectionException;
import jmine.tec.security.ldap.exception.CredentialsNotFoundException;
import jmine.tec.security.ldap.exception.IncorrectPasswordException;
import jmine.tec.security.ldap.exception.LdapGroupNotFoundException;
import jmine.tec.security.ldap.exception.PermissionsNotCreatedException;
import jmine.tec.security.ldap.exception.UserNotFoundException;
import jmine.tec.security.ldap.exception.UserOrPasswordBlankException;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.core.AuthenticationErrorCallback;
import org.springframework.ldap.core.LdapTemplate;

import bancosys.tec.persist.controller.TransactionalController;
import bancosys.tec.security.authorization.Permission;
import bancosys.tec.security.impl.dao.CredentialDAO;
import bancosys.tec.security.impl.domain.Credential;

/**
 * Realiza o login
 * 
 * @author seiti
 * @created Apr 28, 2011
 */
public class LoginDelegate {

    private static final Logger LOG = LogManager.getLogger(LoginDelegate.class);
    
    private final LdapTemplate ldapTemplate;
    
    private final LdapConnector ldapConnector;
    
    private final TransactionalController transactionalController;     
    
    private final CredentialDAO credentialDAO;
    
    /**
     * @return the ldapTemplate 
     */
    public LdapTemplate getLdapTemplate() {
        return ldapTemplate;
    }

    /**
     * @return the ldapConnector 
     */
    public LdapConnector getLdapConnector() {
        return ldapConnector;
    }
    
    /**
     * @return the transactionalController 
     */
    protected TransactionalController getTransactionalController() {
        return this.transactionalController;
    }
    
    /**
     * @return the credentialDAO 
     */
    protected CredentialDAO getCredentialDAO() {
        return this.credentialDAO;
    }
    
    /**
     * Ctor
     * 
     * @param ldapTemplate ldapTemplate
     * @param ldapConnector ldapConnector
     * @param transactionalController transactionalController
     * @param credentialDAO credentialDAO
     */
    public LoginDelegate(LdapTemplate ldapTemplate, LdapConnector ldapConnector, TransactionalController transactionalController, CredentialDAO credentialDAO) {
        this.ldapTemplate = ldapTemplate;
        this.ldapConnector = ldapConnector;
        this.transactionalController = transactionalController;  
        this.credentialDAO = credentialDAO;
    }
    
    /**
     * Autentica e autoriza o usuário
     * 
     * @param username username
     * @param password password
     * @return se autenticado e autorizado traz as lista de permissões encontradas para o usuário
     * @throws IncorrectPasswordException senha incorreta 
     * @throws AuthenticationException caso a autenticação falhe
     * @throws UserNotFoundException usuário não encontrado
     * @throws LdapGroupNotFoundException não encontrou grupos para o usuário no LDAP
     * @throws ConnectionException caso não consiga conectar com o servidor LDAP
     * @throws AuthenticationException não autenticou antes de autorizar
     * @throws CredentialsNotFoundException não encontrou credenciais no sistema para os grupos encontrados
     * @throws PermissionsNotCreatedException permissões não puderam ser criadas
     * @throws UserOrPasswordBlankException usuário e/ou senha não fornecidos
     */
    public Set<Permission> authenticateAndAuthorize(String username, String password)
        throws AuthenticationException, IncorrectPasswordException, UserNotFoundException, ConnectionException, LdapGroupNotFoundException,
            CredentialsNotFoundException, PermissionsNotCreatedException, UserOrPasswordBlankException {

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new UserOrPasswordBlankException();
        }
        
        ErrorCallback errorCallback = new ErrorCallback();
        try {
            if (!authenticate(username, password, errorCallback)) {
                if (userFound(username)) {
                    throw new IncorrectPasswordException(errorCallback.getError());
                } else {
                    throw new UserNotFoundException(username);
                }
            }
        } catch (CommunicationException e) {
            LOG.error("Exception when querying LDAP server.", e);
            throw new ConnectionException(e);
        } catch (RuntimeException e) {
            LOG.error("Exception when authenticating user: " + username, e);
            throw new AuthenticationException(e);
        }
        
        this.getTransactionalController().startTransaction();
        try {
            List<String> groups = getGroups(username);
            if (groups.isEmpty()) {
                throw new LdapGroupNotFoundException();
            }
            
            List<Credential> credentials = getCredentials(groups);
            if (credentials.isEmpty()) {
                throw new CredentialsNotFoundException(username);
            }
            
            final Set<Permission> permissions = new HashSet<Permission>();
            for (Credential credential : credentials) {
                permissions.add(credential.createPermission());
            }

            if (permissions.isEmpty()) {
                LOG.warn("No permissions created for user: " + username);
                throw new PermissionsNotCreatedException(username);
            }

            return permissions;
        } finally {
            this.getTransactionalController().rollback();
        }    
    }
    
    /**
     * @param username username
     * @param password password
     * @param errorCallback callback a ser executado em caso de erros
     * @return se autenticou ou não
     */
    protected boolean authenticate(String username, String password, ErrorCallback errorCallback) {
        LdapConnector c = this.getLdapConnector();
        return this.getLdapTemplate().authenticate(c.getUserBaseDN(), c.getUserFilter(username), password, errorCallback); 
    }
    
    /**
     * @param username username
     * @return if username was found on LDAP 
     */
    protected boolean userFound(String username) {
        LdapConnector c = this.getLdapConnector();
        
        @SuppressWarnings("unchecked")
        List<String> userAttributes = (List<String>) this.getLdapTemplate().search(c.getUserBaseDN(), c.getUserFilter(username), c.getUserMapper());
        
        if (userAttributes.size() > 0) {
            LOG.warn(String.format("Could not authenticate user '%1$s', but he was found on LDAP with this attributes: %2$s",
                    username, StringUtils.join(userAttributes, "; ")));
            return true;
        } else {
            LOG.warn(String.format("Could not authenticate user '%1$s' and he was not found.  Base DN: %2$s, Filter: %3$s, User: %1$s",
                    username, c.getUserBaseDN(), c.getUserFilter(username)));
            return false;
        }
    }

    /**
     * @param username username
     * @return lista com os nomes dos grupos
     */
    protected List<String> getGroups(String username) {
        LdapConnector c = this.getLdapConnector();
        @SuppressWarnings("unchecked")
        List<String> groups = (List<String>) this.getLdapTemplate().search(c.getGroupBaseDN(), c.getGroupsFilter(username), c.getGroupMapper());
        
        if (groups.size() == 0) {
            LOG.warn(String.format("No groups found on LDAP search. Base DN: %s, Filter: %s, User: %s",
              c.getGroupBaseDN(), c.getGroupsFilter(username), username));
          }
        
        return groups;
    }

    /**
     * @param groupNames the groupNames
     * @return lista com as credenciais encontradas
     */
    protected List<Credential> getCredentials(List<String> groupNames) {
        List<Credential> credentials = this.getCredentialDAO().findByGroupNames(groupNames);
        if (credentials.isEmpty()) {
            LOG.warn(String.format("No credentials found on DB for groups: %s", StringUtils.join(groupNames, ", ")));
        }
        return credentials; 
    }
    
    /**
     * Coleta exceções para dispor depois 
     */
    protected static class ErrorCallback implements AuthenticationErrorCallback {
        private Exception error;
        /**
         * Ctor
         * 
         */
        public ErrorCallback() {
            // vazio
        }
        /**
         * @param e a exceção 
         */
        public void execute(Exception e) {
          this.error = e;
        }
        /**
         * @return a exceção coletada
         */
        public Exception getError() {
          return error;
        }        
    }
}

